module Orator

  class Router

    # Initialize the router.  Accepts a block and runs it in the instance,
    # allowing it access to methods such as [#map].
    def initialize(&block)
      @maps = Set.new
      instance_exec &block if block_given?
    end

    # This maps an event to its class.  If a block is given instead, it maps
    # to that.  Classes are not referenced until needed, instead identified by
    # their symbol.
    #
    # @param name [String] the name of the event, in "orator#method" format.
    # @param options [Hash]
    # @example
    #   map "socket#open", to: SocketOrator
    # @example
    #   map "test#echo" # implicit `to: TestOrator`
    # @example
    #   map "test#ping", to: lambda { |s| send message('test.pong') }
    # @return [Hash] the options
    def map(name, options = {})
      orator, method = name.split('#')
      options[:to]     ||= "#{_camelize(orator)}Orator"
      options[:method] ||= method.gsub('.', '_')
      options[:event]  ||= name.gsub('#', '.')

      raise UnknownMapError unless \
        [Class, String, Proc].include? options[:to].class

      map = options.dup
      @maps << map
      map
    end

    alias :event :map
    alias :match :map

    # This finds an event by looking it up in the maps.  If found, it returns
    # the hash that it is associated with.  This will always contain the
    # +:to+, +:method+, and +:event+ keys.  It will select the first one it
    # matches, so the first one mapped gets priority.
    #
    # @return [Hash, nil] the options for the event.
    def find(event)
      @maps.select { |opts| opts[:event] == event }.first
    end

    # Map for a router.  This just calls [::new] and appends the router to the
    # router list.
    #
    # @yields []
    # @return [Router]
    def self.draw(&block)
      router = new(&block)
      routers << router
    end

    # The set of routers that have been mapped.  In a normal application, this
    # would have the length of either one or zero.
    #
    # @return [Set<Router>]
    def self.routers
      @routers ||= Set.new
    end

    private

    def _camelize(string)
      string.gsub(/(?:\A|\_)([A-Za-z])/) do |c|
        if c[0] == "_" then c[1] else c[0] end.upcase
      end.gsub(/\/([A-Za-z])/) { |c| "::#{c[1].upcase}" }
    end

  end

  class UnknownMapError < StandardError; end

end
